﻿using System;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using LinFu.IoC.Configuration;
using LinFu.Proxy.Interfaces;
using LinFu.Reflection.Emit;
using Mono.Cecil;
using Mono.Cecil.Cil;
using LinFu.IoC.Interfaces;
using LinFu.Reflection;
using CoreEx.Common.Extensions;
using FieldAttributes=Mono.Cecil.FieldAttributes;
using MethodAttributes=Mono.Cecil.MethodAttributes;

namespace CoreEx.Common.Implementation.Ioc
{
        [Implements(typeof(IMethodBodyEmitter),ServiceName = "INotifyPropertyChanged")]
        public class NotifyPropertyChangedEmitter : IMethodBodyEmitter, IInitialize<IServiceRequestResult>
        {
            private TypeDefinition _baseType;
            private FieldDefinition _eventHander;
            private MethodDefinition _onPropertyChangedMethod;


            public NotifyPropertyChangedEmitter(TypeDefinition typeDefinition)
            {
                _baseType = typeDefinition;
            }

            public void Emit(MethodInfo originalMethod, MethodDefinition targetMethod)
            {
                if (originalMethod.Name == "add_PropertyChanged")
                    EmitAddHandler(targetMethod);
                else
                    EmitRemoveHandler(targetMethod);
            }

            protected virtual void EmitAddHandler(MethodDefinition targetMethod)
            {         
                var combineMethod = targetMethod.DeclaringType.Module.ImportMethod<Delegate>("Combine",new []{typeof(Delegate),typeof(Delegate)});             
                CilWorker cilWorker = targetMethod.GetILGenerator();
                cilWorker.Emit(OpCodes.Ldarg_0);
                cilWorker.Emit(OpCodes.Dup);
                cilWorker.Emit(OpCodes.Ldfld, _eventHander);
                cilWorker.Emit(OpCodes.Ldarg_1);
                cilWorker.Emit(OpCodes.Call, combineMethod);
                cilWorker.Emit(OpCodes.Castclass, _eventHander.FieldType);
                cilWorker.Emit(OpCodes.Stfld, _eventHander);
                cilWorker.Emit(OpCodes.Ret);
            }

            protected virtual void EmitRemoveHandler(MethodDefinition targetMethod)
            {
                var removeMethod = targetMethod.DeclaringType.Module
                    .ImportMethod<Delegate>("Remove", new [] { typeof(Delegate), typeof(Delegate) });             
                CilWorker cilWorker = targetMethod.GetILGenerator();
                cilWorker.Emit(OpCodes.Ldarg_0);
                cilWorker.Emit(OpCodes.Dup);
                cilWorker.Emit(OpCodes.Ldfld, _eventHander);
                cilWorker.Emit(OpCodes.Ldarg_1);
                cilWorker.Emit(OpCodes.Call, removeMethod);
                cilWorker.Emit(OpCodes.Castclass, _eventHander.FieldType);
                cilWorker.Emit(OpCodes.Stfld, _eventHander);
                cilWorker.Emit(OpCodes.Ret);
            }

        
            /// <summary>
            /// Emits the OnPropertyChanged method that is called by every property setter
            /// </summary>
            /// <remarks>
            /// The code generated at runtime is equalent to the code snippet below. 
            /// </remarks>        
            /// <code>
            ///private void OnPropertyChanged(string propertyName)
            ///{
            ///    if (_propertyChangedEventHandler != null)            
            ///        _propertyChangedEventHandler(this, new PropertyChangedEventArgs(propertyName));            
            ///}
            /// </code>
            protected virtual void EmitOnPropertyChanged()
            {

                var constructor = _baseType.Module.ImportConstructor<PropertyChangedEventArgs>(new [] {typeof (string)});

                var invokeMethod = _baseType.Module.ImportMethod<PropertyChangedEventHandler>("Invoke");

            
                _onPropertyChangedMethod = _baseType.DefineMethod("OnPropertyChanged", MethodAttributes.Private | MethodAttributes.HideBySig, typeof(void),
                                                       new [] {typeof (string)},new Type[]{});
                _onPropertyChangedMethod.Body.InitLocals = true;                        
            
            
                var cilWorker = _onPropertyChangedMethod.GetILGenerator();
            
                _onPropertyChangedMethod.AddLocal(typeof (bool));

                var returnInstruction = cilWorker.Create(OpCodes.Ret);

                //Load the current instance onto the evaluation stack
                cilWorker.Emit(OpCodes.Ldarg_0);
                //Load the field member representing the event handler
                cilWorker.Emit(OpCodes.Ldfld, _eventHander);            
                //Load a null value
                cilWorker.Emit(OpCodes.Ldnull);
                //Pushes the value of 1 onto the evaluation stack if the values are equal, otherwise 0
                cilWorker.Emit(OpCodes.Ceq);
                //Pops the result of the equals operation off the evaluation stack and stores it in the local varible at index 0 
                cilWorker.Emit(OpCodes.Stloc_0);
                //Load the value at local variable index 0 onto the evaluation stack
                cilWorker.Emit(OpCodes.Ldloc_0);
                //Transfers controls to the return instruction of the value is not 0  
                cilWorker.Emit(OpCodes.Brtrue_S, returnInstruction);
                //Load the current instance onto the evaluation stack
                cilWorker.Emit(OpCodes.Ldarg_0);
            
                //Load the field member representing the event handler
                cilWorker.Emit(OpCodes.Ldfld, _eventHander);

                //Load the current instance onto the evaluation stack (object sender)
                cilWorker.Emit(OpCodes.Ldarg_0);
            
                //Load the first parameter onto the evaluation stack (string propertyName)
                cilWorker.Emit(OpCodes.Ldarg_1);

                //Create a new instance of the PropertyChangedEventArgs class
                cilWorker.Emit(OpCodes.Newobj, constructor);

                //Invoke the event handler
                cilWorker.Emit(OpCodes.Callvirt, invokeMethod);

                //Return from the OnPropertyChanged method.
                cilWorker.Append(returnInstruction);
            }



            protected virtual void CreateEventHandler()
            {

                var constructor = _baseType.Module.ImportConstructor<NonSerializedAttribute>();

                var nonSerializedAttribute = new CustomAttribute(constructor);
            
                var fieldType = _baseType.Module.Import(typeof(PropertyChangedEventHandler));
                _eventHander = new FieldDefinition("_propertyChangedEventHandler", fieldType, FieldAttributes.Private);                                                     
                _eventHander.CustomAttributes.Add(nonSerializedAttribute);
            

                _baseType.Fields.Add(_eventHander);           
            }


            protected virtual void PatchProperties()
            {
                _baseType.Properties.Cast<PropertyDefinition>().Where(p => p.SetMethod  != null).Select(p => p.SetMethod)
                    .ForEach(PatchPropertySetter);
            }

            protected virtual void PatchPropertySetter(MethodDefinition setterMethod)
            {
                var propertyName = setterMethod.Name.Substring(4);
            
                var cilWorker = setterMethod.GetILGenerator();

                //Find the instruction that represents OpCodes.Ret
                Instruction returnInstruction = setterMethod.Body.Instructions[setterMethod.Body.Instructions.Count - 1];
            
                //Temporarily remove the return instruction  
                cilWorker.Remove(returnInstruction);

                //Load the instance onto the evaluation stack
                cilWorker.Emit(OpCodes.Ldarg_0);

                //Load the property name onto the evaluation stack
                cilWorker.Emit(OpCodes.Ldstr, propertyName);

                //Call the OnPropertyChanged method
                cilWorker.Emit(OpCodes.Call, _onPropertyChangedMethod);

                //Re-insert the return instruction.
                cilWorker.Append(returnInstruction);
            }



            public void Initialize(IServiceRequestResult source)
            {
                CreateEventHandler();
                EmitOnPropertyChanged();
                PatchProperties();
            }
        }
}
